package com.ar4j;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.sql.DataSource;

import org.antlr.stringtemplate.StringTemplate;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.transaction.interceptor.TransactionAttribute;

import com.ar4j.sql.SqlUtils;
import com.ar4j.type.ITypeConverter;
import com.ar4j.util.ObjectCache;

/**
 * Holds meta data about an active record domain class
 */
public final class ActiveRecordMetadata {
  private static final StringTemplate COUNT_QUERY_TEMPLATE = new StringTemplate(
      "SELECT COUNT(*) FROM $qualifiedTable$ T $where$"
    ); 
  
  private static final StringTemplate DELETE_QUERY_TEMPLATE = new StringTemplate(
      "DELETE FROM $qualifiedTable$ $where$"
    );
  
  private static final String INSERT_CACHE_NAME = DefaultActiveRecord.class.getName() + ".insert";
  private static final ObjectCache<InsertCacheKey, SimpleJdbcInsert> INSERT_CACHE = 
    new ObjectCache<InsertCacheKey, SimpleJdbcInsert>(INSERT_CACHE_NAME, 1024, 0);
  
  private Class<?> baseClass;
  private Set<String> propertyNames;
  private Map<String, Class<?>> propertyTypeMap;
  private Map<Method, ActiveRecordMethodType> methodTypeMap;
  private Map<Method, Method> defaultImplementationMethodMap;
  private Map<Method, DynamicMethodMetadata> dynamicMethodMetadataMap;
  private Map<String, Method> propertyReadMethods;
  private Map<String, Method> propertyWriteMethods;
  private String tableName;
  private String identifierPropertyName;
  private Map<String, ITypeConverter<?, ?>> propertyTypeConvertorMap;
  private Map<String, String> propertyColumnNameMap;
  private Map<Method, TransactionAttribute> methodTransactionAttributeMap;
  
  private boolean initialized;
  private StringTemplate selectQueryTemplate;
  private StringTemplate updateQueryTemplate;
  
  /**
   * Initializes the metadata object, creating queries and any other objects that may be useful in usage
   */
  public void init() {
    if(initialized) {
      return;
    }
    
    // initialize select query
    StringBuilder sql = new StringBuilder();
    sql.append("SELECT ");
    SqlUtils.appendCommaDelimitedColumnList(sql, "T", propertyColumnNameMap.values());
    sql.append(" FROM $qualifiedTable$ ");
    sql.append(" T $suffix$");
    this.selectQueryTemplate = new StringTemplate(sql.toString());
    
    // initialize update query
    sql = new StringBuilder();
    sql.append("UPDATE $qualifiedTable$ ");
    sql.append(" SET ");
    
    boolean first = true;
    for(Map.Entry<String, String> entry : propertyColumnNameMap.entrySet()) {
      if(identifierPropertyName.equals(entry.getKey())) {
        continue;
      }
      
      if(!first) {
        sql.append(", ");
      } else {
        first = false;
      }
     
      sql.append(entry.getValue());
      sql.append(" = :");
      sql.append(entry.getValue());
    }
    
    List<String> noCollectionFields = Collections.emptyList();
    SqlUtils.appendWhereClause(sql, Collections.singletonList(identifierPropertyName), noCollectionFields, null, propertyColumnNameMap);
    this.updateQueryTemplate = new StringTemplate(sql.toString());
    
    initialized = true;
  }
  
  /**
   * Retrieves a SimpleJdbcInsert instance for the given metadata/context combination and the current instance's base class
   */ 
  public SimpleJdbcInsert getSimpleJdbcInsertForDataSourceAndSchema(DataSource dataSource, String schemaName) {
    
    // create the insert if needed
    InsertCacheKey key = new InsertCacheKey(getBaseClass(), dataSource, schemaName);
    SimpleJdbcInsert insert = INSERT_CACHE.get(key);
    if(insert == null) {
      String[] columns = new String[propertyNames.size() - 1];
      int i = 0;
      for(Map.Entry<String, String> entry : propertyColumnNameMap.entrySet()) {
        if(identifierPropertyName.equals(entry.getKey())) {
          continue;
        }
        
        columns[i] = entry.getValue();
        i++;
      }
      
      insert = (SimpleJdbcInsert) new SimpleJdbcInsert(key.getDataSource())
        .withoutTableColumnMetaDataAccess()
        .withSchemaName(key.getSchemaName())  
        .withTableName(tableName)
        .usingGeneratedKeyColumns(identifierPropertyName)
        .usingColumns(columns);
      
      INSERT_CACHE.put(key, insert);
    }
    
    return insert;
  }
  
  public StringTemplate getSelectQueryTemplate() {
    return selectQueryTemplate;
  }
  
  public StringTemplate getUpdateQueryTemplate() {
    return updateQueryTemplate;
  }
  
  public StringTemplate getDeleteQueryTemplate() {
    return DELETE_QUERY_TEMPLATE;
  }
  
  public StringTemplate getCountQueryTemplate() {
    return COUNT_QUERY_TEMPLATE;
  }

  public Class<?> getBaseClass() {
    return baseClass;
  }

  public Set<String> getPropertyNames() {
    return propertyNames;
  }
  
  public Map<String, Class<?>> getPropertyTypeMap() {
    return propertyTypeMap;
  }

  public Map<Method, ActiveRecordMethodType> getMethodTypeMap() {
    return methodTypeMap;
  }
  
  public Map<Method, Method> getDefaultImplementationMethodMap() {
    return defaultImplementationMethodMap;
  }

  public Map<Method, DynamicMethodMetadata> getDynamicMethodMetadataMap() {
    return dynamicMethodMetadataMap;
  }

  public Map<String, Method> getPropertyReadMethods() {
    return propertyReadMethods;
  }

  public Map<String, Method> getPropertyWriteMethods() {
    return propertyWriteMethods;
  }
  
  public String getTableName() {
    return tableName;
  }

  public String getIdentifierPropertyName() {
    return identifierPropertyName;
  }
    
  public Map<String, ITypeConverter<?, ?>> getPropertyTypeConvertorMap() {
    return propertyTypeConvertorMap;
  }
  
  public Map<String, String> getPropertyColumnNameMap() {
    return propertyColumnNameMap;
  }
  
  public Map<Method, TransactionAttribute> getMethodTransactionAttributeMap() {
    return methodTransactionAttributeMap;
  }

  protected void setBaseClass(Class<?> baseClass) {
    this.baseClass = baseClass;
  }
  
  protected void setPropertyNames(Set<String> propertyNames) {
    this.propertyNames = propertyNames;
  }
  
  protected void setPropertyTypeMap(Map<String, Class<?>> propertyTypeMap) {
    this.propertyTypeMap = Collections.unmodifiableMap(propertyTypeMap);
  }
  
  public void setMethodTypeMap(Map<Method, ActiveRecordMethodType> methodTypeMap) {
    this.methodTypeMap = methodTypeMap;
  }
  
  public void setDefaultImplementationMethodMap(Map<Method, Method> defaultImplementationMethodMap) {
    this.defaultImplementationMethodMap = defaultImplementationMethodMap;
  }
  
  public void setDynamicMethodMetadataMap(Map<Method, DynamicMethodMetadata> dynamicMethodMetadataMap) {
    this.dynamicMethodMetadataMap = dynamicMethodMetadataMap;
  }
  
  protected void setPropertyReadMethods(Map<String, Method> propertyReadMethods) {
    this.propertyReadMethods = Collections.unmodifiableMap(propertyReadMethods);
  }
  
  protected void setPropertyWriteMethods(Map<String, Method> propertyWriteMethods) {
    this.propertyWriteMethods = Collections.unmodifiableMap(propertyWriteMethods);
  }
  
  protected void setTableName(String tableName) {
    this.tableName = tableName;
  }
  
  protected void setIdentifierPropertyName(String identifierPropertyName) {
    this.identifierPropertyName = identifierPropertyName;
  }
    
  protected void setPropertyTypeConvertorMap(Map<String, ITypeConverter<?, ?>> propertyTypeConvertorMap) {
    this.propertyTypeConvertorMap = propertyTypeConvertorMap;
  }
  
  protected void setPropertyColumnNameMap(Map<String, String> propertyColumnNameMap) {
    this.propertyColumnNameMap = propertyColumnNameMap;
  }
  
  protected void setMethodTransactionAttributeMap(Map<Method, TransactionAttribute> methodTransactionAttributeMap) {
    this.methodTransactionAttributeMap = methodTransactionAttributeMap;
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
  
  /**
   * Defines the different kinds of methods that can be present in an active record instance
   */
  public static enum ActiveRecordMethodType {
    GET_PROPERTY,
    SET_PROPERTY,
    PASSTHROUGH,
    HANDLED_BY_DEFAULT_IMPLEMENTATION,
    DYNAMIC
    ;
  }
  
  /**
   * Composite object holding the key to the insert cache. Consists of a base class, data source, and a 
   * schema name (which could be null).
   */
  private static class InsertCacheKey {
    private Class<?> baseClass;
    private DataSource dataSource;
    private String schemaName;
    private int hashCode;
    
    public InsertCacheKey(Class<?> baseClass, DataSource dataSource, String schemaName) {
      this.baseClass = baseClass;
      this.dataSource = dataSource;
      this.schemaName = schemaName;
      this.hashCode = HashCodeBuilder.reflectionHashCode(this);
    }

    @SuppressWarnings("unused") public Class<?> getBaseClass() { return baseClass; } // used in reflection!!!
    public DataSource getDataSource() { return dataSource; }
    public String getSchemaName() { return schemaName; }

    @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); }
    @Override public int hashCode() { return hashCode; }
    @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); }
  }
  
}
